using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;

using Atomic.Libraries;

namespace Atomic.Structures
{
	[Serializable]
	[DebuggerDisplay("{DebuggerDisplay}")]
	public class Composition
	{
		[NonSerialized] private int hashCode;
		[NonSerialized] private Dictionary<Atom, double> counts;

		public Composition(IEnumerable<Constituent> constituents)
		{
			if (constituents == null)
			{
				throw new ArgumentNullException();
			}

			Constituents = new ReadOnlyList<Constituent>(constituents);
			Atoms = new ReadOnlyList<Atom>(constituents.Select(c => c.Atom).Distinct());
			TotalCount = constituents.Select(c => c.Count).Sum();
		}

		public Composition(params Constituent[] constituents)
			: this((IEnumerable<Constituent>)constituents)
		{
		}

		public Composition(IEnumerable<Atom> atoms, IEnumerable<double> counts)
			: this(CreateConstituents(atoms.ToArray(), counts.ToArray()))
		{
		}
		
		public Composition(Atom atom1, double count1)
			: this(new Atom[] { atom1 }, new double[] { count1 })
		{
		}
		
		public Composition(Atom atom1, double count1, Atom atom2, double count2)
			: this(new Atom[] { atom1, atom2 }, new double[] { count1, count2 })
		{
		}

		public Composition(Atom atom1, double count1, Atom atom2, double count2, Atom atom3, double count3)
			: this(new Atom[] { atom1, atom2, atom3 }, new double[] { count1, count2, count3 })
		{
		}

		public static Composition Create(Structure structure)
		{
			return new Composition(CreateConstituents(structure));
		}

		public static Composition CreateFractional(Structure structure)
		{
			return Create(structure).ToFractions();
		}

		public static Composition CreateFractionalBinary(Atom atom1, double fraction1)
		{
			return CreateFractionalBinary(atom1, Atom.Vacancy, fraction1);
		}

		public static Composition CreateFractionalBinary(Atom atom1, Atom atom2, double fraction1)
		{
			return new Composition(new Constituent(atom1, fraction1), new Constituent(atom2, 1.0 - fraction1));
		}
	
		public static Composition CreateFractionalTernary(Atom atom1, Atom atom2, double fraction1, double fraction2)
		{
			return CreateFractionalTernary(atom1, atom2, Atom.Vacancy, fraction1, fraction2);
		}

		public static Composition CreateFractionalTernary(Atom atom1, Atom atom2, Atom atom3, double fraction1, double fraction2)
		{
			return new Composition(new Constituent(atom1, fraction1), new Constituent(atom2, fraction2), new Constituent(atom3, 1.0 - fraction1 - fraction2));
		}

		private static IEnumerable<Constituent> CreateConstituents(Structure structure)
		{
			Dictionary<Atom, int> constituents = new Dictionary<Atom, int>();

			foreach (Site site in structure.Sites)
			{
				if (!constituents.ContainsKey(site.Atom))
				{
					constituents.Add(site.Atom, 0);
				}

				constituents[site.Atom] += 1;
			}

			foreach (KeyValuePair<Atom, int> constituent in constituents)
			{
				yield return new Constituent(constituent.Key, constituent.Value);
			}
		}

		private static IEnumerable<Constituent> CreateConstituents(Atom[] atoms, double[] counts)
		{
			int n = atoms.Length;

			if (counts.Length != n)
			{
				throw new ArgumentException();
			}

			for (int i = 0; i < n; i++)
			{
				yield return new Constituent(atoms[i], counts[i]);
			}
		}

		public Composition Scale(double scaling)
		{
			return new Composition(Constituents.Select(c => new Constituent(c.Atom, c.Count * scaling)));
		}

		public Composition ToFractions()
		{
			return Scale(1.0 / TotalCount);
		}

		public double Count(Atom atom)
		{
			if (counts == null)
			{
				counts = new Dictionary<Atom, double>();
				foreach (Constituent c in Constituents)
				{
					if (!counts.ContainsKey(c.Atom))
					{
						counts[c.Atom] = 0.0;
					}

					counts[c.Atom] += c.Count;
				}
			}

			double count;
			if (!counts.TryGetValue(atom, out count))
			{
				count = 0.0;
			}

			return count;
		}

		public double Fraction(Atom atom)
		{
			return Count(atom) / TotalCount;
		}

		public bool Equals(Composition other)
		{
			if (object.ReferenceEquals(other, null))
			{
				return false;
			}

			// Force computing hash codes.
			GetHashCode();
			other.GetHashCode();

			//return EqualityHelper.SymmetricEquals(this, other, Constituents.ToArray(), other.Constituents.ToArray(), hashCode, other.hashCode);
			return EqualityHelper.Equals(this, other, new object[] { new SymmetricEquatableEnumerable<Constituent>(Constituents) }, new object[] { new SymmetricEquatableEnumerable<Constituent>(other.Constituents) }, hashCode, other.hashCode);
		}

		public override bool Equals(object other)
		{
			return Equals(other as Composition);
		}

		public override int GetHashCode()
		{
			if (hashCode == 0)
			{
				//hashCode = EqualityHelper.SymmetricHashCode(Constituents.ToArray());
				hashCode = EqualityHelper.HashCode(new object[] { new SymmetricEquatableEnumerable<Constituent>(Constituents) });
			}

			return hashCode;
		}

		public ReadOnlyList<Constituent> Constituents
		{
			get;
			private set;
		}

		public ReadOnlyList<Atom> Atoms
		{
			get;
			private set;
		}

		public double TotalCount
		{
			get;
			private set;
		}

		private string DebuggerDisplay
		{
			get
			{
				return string.Join(", ", Constituents.Select(c => string.Format(CultureInfo.InvariantCulture, "{0} = {1:f4}", c.Atom.Symbol, c.Count)));
			}
		}

		public static bool operator ==(Composition object1, Composition object2)
		{
			if (object.ReferenceEquals(object1, null))
			{
				if (object.ReferenceEquals(object2, null))
				{
					return true;
				}

				return false;
			}

			return object1.Equals(object2);
		}

		public static bool operator !=(Composition object1, Composition object2)
		{
			return !(object1 == object2);
		}
	}
}
